When writing code during the development of software applications, developers commonly spend a significant amount of time “debugging” the code to find runtime errors in the code. In doing so, developers may take several approaches to reproduce and localize a source code bug, such as observing behavior of a program based on different inputs, inserting debugging code (e.g., to print variable values, to track branches of execution, etc.), temporarily removing code portions, etc. Tracking down runtime errors to pinpoint code bugs can occupy a significant portion of application development time.
Many types of debugging applications (“debuggers”) have been developed in order to assist developers with the code debugging process. These tools offer developers the ability to trace, visualize, and alter the execution of computer code. For example, debuggers may visualize the execution of code instructions, may present variable values at various times during code execution, may enable developers to alter code execution paths, and/or may enable developers to set “breakpoints” and/or “watchpoints” on code elements of interest (which, when reached during execution, causes execution of the code to be suspended), among other things. In general, a “breakpoint” refers to a memory location storing a specified portion of executable code (e.g., a line of code, a variable definition, a function definition, etc.), while a “watchpoint” refers to a memory location storing data (e.g., a memory location storing the value a variable, constant, data structure, etc.). For simplicity herein, use of the term “breakpoint” refers to both breakpoints and watchpoints.
An emerging form of debugging applications enable “time travel,” “reverse,” or “historic” debugging, in which execution of a program is recorded/traced by tracing software and/or hardware into one or more trace files, which can be then be used to replay execution of the program later for forward and backward analysis. For example, “time travel” debuggers can enable a developer to set forward breakpoints (like conventional debuggers) as well as reverse breakpoints.
One challenge in implementing “time travel” debuggers involves how to detect when a memory address of interest (e.g., corresponding to a breakpoint) has been encountered during replay of a trace file in a flexible and performant manner. One mechanism that has been explored for tracking memory addressees of interest during replay is to use processor-implemented hardware breakpoints. While performant, use of hardware breakpoints has the disadvantage of severely limiting the number of memory addresses that can be concurrently watched for. For example, the INTEL x86 architecture provides only four debug registers (i.e., DR0 to DR3) for storing memory addresses to watch for. Use of hardware breakpoints also has the disadvantage of inherently binding a debugger implementation to specifically-supported hardware (e.g., by relying on the x86 debug registers).
Another mechanism that has been explored is to use software breakpoints, in which each traced operation that accesses memory (e.g., a read or a write to memory storing data, or a read/fetch for execution from memory storing program code) is instrumented with additional code instructions that check whether or not the access is to a memory address of interest. However, instrumentation has the severe disadvantage of causing a check for the occurrence of a breakpoint to be performed for every memory access, regardless of whether or not that memory access would actually correspond to a breakpoint. In many environments, execution of the instrumented code instructions results in the use of tens to hundreds of additional processor cycles for each memory access, which has a significant detrimental effect on replay performance that is perceivable by a human user.
Previous solutions by the inventor to address the foregoing challenges through use of caches (e.g., a cache of a hardware or emulated processor, or some other cache memory structure) as part of breakpoint detection. These solutions greatly limit the number of processor cycles needed to perform a breakpoint check as compared to instrumentation, and are described in U.S. patent application Ser. No. 15/405,849, filed Jan. 13, 2017, and titled “EFFICIENT BREAKPOINT DETECTIONS VIA CACHES,” the entire contents of which are incorporated by reference in their entirety.
These solutions use a cache to limit breakpoint checks to only memory accesses that cause cache misses to occur, rather than performing a breakpoint check on every memory access. These solutions can offer great performance improvements over non-cache-based breakpoint detection solutions, while providing a virtually limitless number of breakpoints. They involve evicting a cache line if it overlaps with a memory address that corresponds to a breakpoint, and then “single stepping” the processor on each cache miss in order to determine if a breakpoint was encountered. While being an advancement over instrumentation for breakpoint detection, these solutions severely impact execution performance when cache misses occur (even if no breakpoint was hit in connection with the cache miss), and also cause cache misses to occur more frequently than they might absent use of the cache for breakpoint detection (i.e., because they proactively evict cache lines that overlap with a watched-for memory address).